package se.thoka.twt.client.widgets;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import se.thoka.twt.client.event.ListItemClickedEvent;
import se.thoka.twt.client.event.ListItemClickedHandler;
import se.thoka.twt.client.event.ListItemMouseOutEvent;
import se.thoka.twt.client.event.ListItemMouseOutHandler;
import se.thoka.twt.client.event.ListItemMouseOverEvent;
import se.thoka.twt.client.event.ListItemMouseOverHandler;
import se.thoka.twt.client.utils.CSS;
import se.thoka.twt.client.utils.JSNIUtil;
import se.thoka.twt.client.utils.TWTListBase;

import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;

public class TWTList<LI extends IsWidget> extends Composite implements TWTListBase<LI> {
	public static final String CSS_LIST 		= "twt-List";
	public static final String CSS_LIST_ITEM 	= "twt-ListItem";

	private static final int LIST_ITEM_EVENT_MASK = Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT;

	/** The root wrapper panel, used for quick clear of entire list */
	private SimplePanel rootPanel;
	/** The actual list panel containing the {@link ListItemWrapper}s */
	private FlowPanel listPanel;
	/** The mirror list of the list item widgets */
	private ArrayList<LI> mirrorList;

	private boolean stripe = false;
	
	private EditLog editLog;

	/**
	 * Creates a new instance of {@link TWTList}.
	 */
	public TWTList() {
		initWidget(rootPanel = new SimplePanel());
		rootPanel.setStylePrimaryName(CSS_LIST);
		JSNIUtil.setSelectionEnabled(false, rootPanel.getElement());
		
		clear();
	}

	public void add(LI listItem) {
		ListItemWrapper liw = new ListItemWrapper(listItem);
//		if (contains(listItem)) { // OPTIMIZE (IGNORE) UNTIL PROBLEM OCCURS
//			throw new IllegalArgumentException("List item already in list");
//		}
		int addIdx = mirrorList.size();
		mirrorList.add(listItem);
		listPanel.add(liw);
		if (editLog == null) {
			if (isStripeEnabled()) {
				stripe(listItem, getItemCount());
			}
//			fireEvent(new ListItemsAddedEvent<LI>(classType, listItem, addIdx));
		} else {
			editLog.added(listItem, addIdx);
		}
	};

	public void addAll(Collection<LI> listItems) {
		int addIdx = mirrorList.size();
		for (LI listItem : listItems) {
//			if (contains(listItem)) { // OPTIMIZE (IGNORE) UNTIL PROBLEM OCCURS
//				throw new IllegalArgumentException("List item already in list");
//			}
			ListItemWrapper wrapper = new ListItemWrapper(listItem);
			listPanel.add(wrapper);
			mirrorList.add(listItem);
		}
		if (!listItems.isEmpty()) {
			if (editLog == null) {
				if (isStripeEnabled()) {
					reStripeAll(getItemCount());
				}
//				fireEvent(new ListItemsAddedEvent<LI>(classType, listItems, addIdx));
			} else {
				editLog.added(listItems, addIdx);
			}
		}
	}

	public boolean contains(LI listItem) {
		return mirrorList.contains(listItem);
	}

	public LI getItem(int index) {
		return mirrorList.get(index);
	}

	public int getItemCount() {
		return mirrorList.size();
	}

	public int getItemIndex(LI listItem) {
		return mirrorList.indexOf(listItem);
	}

	public void insert(LI listItem, int index) {
		if (contains(listItem)) {
			throw new IllegalArgumentException("List item already in list");
		}
		
		int startIndex = getItemCount();
		ListItemWrapper wrapper = new ListItemWrapper(listItem);
		listPanel.insert(wrapper, index);
		mirrorList.add(index, listItem);
		if (editLog == null) {
			if (isStripeEnabled()) {
				reStripeAll(startIndex);
			}
//			fireEvent(new ListItemsAddedEvent<LI>(classType, listItem, index));
		} else {
			editLog.added(listItem, index);
		}
	}

	public boolean isEmpty() {
		return mirrorList.isEmpty();
	}
	
	/**
	 * @return <code>true</code> if edit mode is currently active
	 */
	public boolean isEditMode() {
		return editLog != null;
	}

	/**
	 * @return <code>true</code> if striping of items is enabled
	 */
	public boolean isStripeEnabled() {
		return stripe;
	}

	public boolean remove(int listIndex) {
		boolean removed = listPanel.remove(listIndex);
		if (removed) {
			LI w = mirrorList.get(listIndex);
			mirrorList.remove(listIndex);
			
			if (editLog == null) {
				if (isStripeEnabled()) {
					reStripeAll(listIndex);
				}
//				fireEvent(new ListItemsRemovedEvent<LI>(classType, w, listIndex));
			} else {
				editLog.removed(w, listIndex);
			}
		}
		return removed;
	}

	public boolean remove(LI listItem) {
		int index = mirrorList.indexOf(listItem);
		boolean removed = index != -1 && listPanel.remove(index);
		if (removed) {
			mirrorList.remove(listItem);
			if (editLog == null) {
				if (isStripeEnabled()) {
					reStripeAll(index);
				}
//				fireEvent(new ListItemsRemovedEvent<LI>(classType, listItem, index));
			} else {
				editLog.removed(listItem, index);
			}
		}
		return removed;
	}

	public void removeAll(Collection<LI> listItems) {
		List<LI> removedWidgets = new ArrayList<LI>();
		int removedIdx = -1;
		for (LI listItem : listItems) {
			if (listPanel.remove(listItem.asWidget().getParent())) {
				if (removedIdx == -1) {
					removedIdx = mirrorList.indexOf(listItem);
				}
				mirrorList.remove(listItem);
				removedWidgets.add(listItem);
			}
		}
		if (!removedWidgets.isEmpty()) {
			if (editLog == null) {
				if (isStripeEnabled()) {
					reStripeAll();
				}
//				fireEvent(new ListItemsRemovedEvent<LI>(classType, removedWidgets, removedIdx));
			} else {
				editLog.removed(removedWidgets, removedIdx);
			}
		}
	}
	
	/**
	 * Sets this list to edit mode, i.e. makes it faster to edit the list.
	 * <p>Note: Handlers will be called AFTER edit mode has been turned off</p>
	 * @param editing Set <code>true</code> to activate edit mode
	 */
	public void setEditMode(boolean editing) {
		if (editLog == null && editing) {
			editLog = new EditLog();
		} else if (editLog != null && !editing) {
			if (editLog.hasAdded()) {
//				fireEvent(new ListItemsAddedEvent<LI>(classType, editLog.getAdded(), editLog.getAddedIdx()));
			}
			if (editLog.hasRemoved()) {
//				fireEvent(new ListItemsRemovedEvent<LI>(classType, editLog.getRemoved(), editLog.getRemovedIdx()));
			}
			
			// Turn off
			editLog = null;
			
			if (isStripeEnabled()) {
				reStripeAll();
			}
		}
	}

	/**
	 * @param enabled Set <code>true</code> to enable striping of items
	 */
	public void setStripeEnabled(boolean enabled) {
		stripe = enabled;
		reStripeAll();
	}

	public List<LI> subList(int fromIndex, int toIndex) {
		List<LI> result = new ArrayList<LI>();
		for (int i = fromIndex; i <= toIndex; i++) {
			result.add(mirrorList.get(i));
		}
		return result;
	}

	public void clear() {
		mirrorList = new ArrayList<LI>();
		rootPanel.setWidget(listPanel = new FlowPanel());

//		fireEvent(new ListItemsClearedEvent<LI>(classType));
		if (editLog != null) {
			editLog.clearAdded();
			editLog.clearRemoved();
		}
	}
	
	@Override
	public HandlerRegistration addListItemClickedHandler(ListItemClickedHandler<LI> handler) {
		return addHandler(handler, ListItemClickedEvent.getType());
	}
	
	@Override
	public HandlerRegistration addListItemMouseOverHandler(ListItemMouseOverHandler<LI> handler) {
		return addHandler(handler, ListItemMouseOverEvent.getType());
	}
	
	@Override
	public HandlerRegistration addListItemMouseOutHandler(ListItemMouseOutHandler<LI> handler) {
		return addHandler(handler, ListItemMouseOutEvent.getType());
	}

	public boolean addListItemStyleDependentName(int index, String styleSuffix) {
		Widget w = listPanel.getWidget(index);
		if (w != null) {
			w.addStyleDependentName(styleSuffix);
		}
		return w != null;
	}

	public boolean addListItemStyleDependentName(LI listItem, String styleSuffix) {
		@SuppressWarnings("unchecked")
		ListItemWrapper wrapper = (ListItemWrapper) listItem.asWidget().getParent();
		if (wrapper != null) {
			wrapper.addStyleDependentName(styleSuffix);
		}
		return wrapper != null;
	}

	public boolean removeListItemStyleDependentName(int index, String styleSuffix) {
		Widget w = listPanel.getWidget(index);
		if (w != null) {
			w.removeStyleDependentName(styleSuffix);
		}
		return w != null;
	}

	public boolean removeListItemStyleDependentName(LI listItem, String styleSuffix) {
		@SuppressWarnings("unchecked")
		ListItemWrapper wrapper = (ListItemWrapper) listItem.asWidget().getParent();
		if (wrapper != null) {
			wrapper.removeStyleDependentName(styleSuffix);
		}
		return wrapper != null;
	}

	/**
	 * Re-stripes all list items
	 */
	protected void reStripeAll() {
		reStripeAll(0);
	}

	/**
	 * Re-stripes all list items
	 * 
	 * @param fromIndex The start index
	 */
	protected void reStripeAll(int fromIndex) {
		reStripeAll(fromIndex, mirrorList.size());
	}

	/**
	 * Re-stripes all list items
	 * 
	 * @param fromIndex The start index
	 * @param endIndex The end index
	 */
	protected void reStripeAll(int fromIndex, int endIndex) {
		final int size = mirrorList.size();
		for (int i = fromIndex; i > 0 && i < size && i < endIndex; i++) {
			stripe(mirrorList.get(i), i);
		}
	}

	/**
	 * Stripes the supplied list item
	 * 
	 * @param listItem The list item
	 */
	protected void stripe(LI listItem) {
		stripe(listItem, getItemIndex(listItem));
	}

	/**
	 * Stripes the list item on the supplied index
	 * 
	 * @param index The index
	 */
	protected void stripe(int index) {
		stripe(getItem(index), index);
	}

	/**
	 * Stripes/unstripes a list item
	 * 
	 * @param listItem The list item, must not be null
	 * @param index The list item index
	 */
	protected void stripe(LI listItem, int index) {
		if (listItem != null) {
			CSS.State.STRIPE.setDependentTo(listItem.asWidget().getParent(), isStripeEnabled() && index % 2 != 0);
		}
	}

	/**
	 * Calls the listeners of an {@link Event}.
	 * 
	 * @param event The event to send.
	 * @param sender The list item sender.
	 */
	private void callEventListeners(Event event, ListItemWrapper sender) {
		LI listItem = sender.getListItem();
		int listIndex = getItemIndex(listItem);

		switch (DOM.eventGetType(event)) {
			case Event.ONCLICK:
//				if (event.getButton() == Event.BUTTON_RIGHT) {
//					event.preventDefault();
//					fireEvent(new ListRightClickEvent<LI>(classType, listItem, listIndex));
//				} else {
					fireEvent(new ListItemClickedEvent<LI>(listItem, listIndex, DOM.eventGetShiftKey(event), DOM.eventGetCtrlKey(event)));
//				}
				break;
//			case Event.ONDBLCLICK:
//				fireEvent(new ListDoubleClickEvent<LI>(classType, listItem, listIndex));
//				break;
//			case Event.ONKEYDOWN:
//				fireEvent(new ListKeyDownEvent<LI>(classType, listItem, listIndex, DOM.eventGetKeyCode(event)));
//				break;
//			case Event.ONKEYPRESS:
//				fireEvent(new ListKeyPressedEvent<LI>(classType, listItem, listIndex, DOM.eventGetKeyCode(event)));
//				break;
//			case Event.ONKEYUP:
//				fireEvent(new ListKeyUpEvent<LI>(classType, listItem, listIndex, DOM.eventGetKeyCode(event)));
//				break;
			case Event.ONMOUSEOVER:
				fireEvent(new ListItemMouseOverEvent<LI>(listItem, listIndex));
				break;
			case Event.ONMOUSEOUT:
				fireEvent(new ListItemMouseOutEvent<LI>(listItem, listIndex));
				break;
//			case Event.ONMOUSEDOWN:
//				fireEvent(new ListMouseDownEvent<LI>(classType, listItem, listIndex, DOM.eventGetButton(event)));
//				break;
//			case Event.ONMOUSEUP:
//				fireEvent(new ListMouseUpEvent<LI>(classType, listItem, listIndex, DOM.eventGetButton(event)));
//				break;
		}
	}

	/**
	 * <code>ListItemWrapper</code>
	 * 
	 * <p>
	 * Wraps a list item widget inside of a <code>mobical-ListItem DIV</code> element and handles the browser events fetched on the list item.
	 * </p>
	 */
	private class ListItemWrapper extends SimplePanel {
		/** The list item widget that is wrapped */
		private final LI listItem;

		/**
		 * Creates a new instance of {@link ListItemWrapper}.
		 * 
		 * @param listItem The list item widget to wrap
		 */
		public ListItemWrapper(LI listItem) {
			setWidget(this.listItem = listItem);
			sinkEvents(LIST_ITEM_EVENT_MASK);
			setStylePrimaryName(CSS_LIST_ITEM);
			// DOM.setElementAttribute(w.getElement(), "unselectable", "on");
			// DOM.setStyleAttribute(w.getElement(), "MozUserSelect", "none");
			// DOM.setStyleAttribute(w.getElement(), "KhtmlUserSelect", "none");
		}

		/**
		 * Returns the wrapped list item widget.
		 * 
		 * @return The wrapped list item widget.
		 */
		public LI getListItem() {
			return listItem;
		}

		@Override
		public void onBrowserEvent(Event event) {
			TWTList.this.callEventListeners(event, this);
		}
	}
	
	private class EditLog {
		private final List<LI> added = new ArrayList<LI>();
		private final List<LI> removed = new ArrayList<LI>();
		private int addedIdx = -1;
		private int removedIdx = -1;
		
		public void added(LI li, int addIdx) {
			if (this.addedIdx == -1) {
				this.addedIdx = addIdx;
			}
			added.add(li);
		}
		
		public void added(Collection<LI> lis, int addIdx) {
			if (this.addedIdx == -1) {
				this.addedIdx = addIdx;
			}
			added.addAll(lis);
		}
		
		public void clearAdded() {
			added.clear();
			addedIdx = -1;
		}
		
		public void clearRemoved() {
			removed.clear();
			removedIdx = -1;
		}
		
//		public Collection<LI> getAdded() {
//			return added;
//		}
//		
//		public int getAddedIdx() {
//			return addedIdx;
//		}
//		
//		public int getRemovedIdx() {
//			return removedIdx;
//		}
//		
//		public Collection<LI> getRemoved() {
//			return removed;
//		}
		
		public boolean hasAdded() {
			return !added.isEmpty();
		}
		
		public boolean hasRemoved() {
			return !removed.isEmpty();
		}
		
		public void removed(LI li, int removedIdx) {
			if (this.removedIdx == -1) {
				this.removedIdx = removedIdx;
			}
			removed.add(li);
		}
		
		public void removed(Collection<LI> lis, int removedIdx) {
			if (this.removedIdx == -1) {
				this.removedIdx = removedIdx;
			}
			removed.removeAll(lis);
		}
	}
}
